Skip to content

feat(spp_api_v2): support HTTP Basic Auth on OAuth token endpoint#69

Open
jeremi wants to merge 3 commits into19.0from
feat/api-v2-oauth-basic-auth
Open

feat(spp_api_v2): support HTTP Basic Auth on OAuth token endpoint#69
jeremi wants to merge 3 commits into19.0from
feat/api-v2-oauth-basic-auth

Conversation

@jeremi
Copy link
Member

@jeremi jeremi commented Mar 5, 2026

Summary

  • Add RFC 6749-compliant client authentication to /oauth/token endpoint
  • Support HTTP Basic Auth header parsing (Authorization: Basic base64(id:secret))
  • Support form-encoded body (application/x-www-form-urlencoded) alongside JSON
  • Add configurable token lifetime via spp_api_v2.token_lifetime_hours system parameter (default 24h)
  • Keep JSON body for backwards compatibility

Why: Needed for QGIS native OAuth2 flow, which sends Basic Auth headers per RFC 6749 Section 2.3.1.

Test plan

  • Existing OAuth tests pass (updated expires_in expectation from 3600 to 86400)
  • Run ./scripts/test_single_module.sh spp_api_v2
  • Verify JSON body still works (backwards compatibility)
  • Verify Basic Auth header works with QGIS

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the OAuth token endpoint by adding support for RFC 6749-compliant client authentication methods, including HTTP Basic Auth and form-encoded bodies, alongside the existing JSON body. It also introduces a configurable token lifetime, allowing administrators to adjust the duration of issued access tokens. These changes are crucial for integrating with clients like QGIS that rely on standard OAuth2 authentication flows.

Highlights

  • HTTP Basic Auth Support: Implemented RFC 6749-compliant client authentication for the /oauth/token endpoint, specifically supporting HTTP Basic Auth headers.
  • Form-Encoded Body Support: Added support for client credentials provided in a form-encoded body (application/x-www-form-urlencoded) to the /oauth/token endpoint.
  • Configurable Token Lifetime: Introduced a configurable token lifetime via the spp_api_v2.token_lifetime_hours system parameter, defaulting to 24 hours, affecting both the expires_in response and the JWT exp claim.
  • Backwards Compatibility: Ensured that the existing JSON body format for client authentication remains supported for backwards compatibility.
Changelog
  • spp_api_v2/routers/oauth.py
    • Added a new internal function _parse_token_request to abstract the parsing of client credentials from HTTP Basic Auth headers, form-encoded bodies, and JSON bodies.
    • Modified the get_token endpoint to utilize the new _parse_token_request function for client credential extraction.
    • Updated the get_token function to retrieve a configurable token lifetime from system parameters, applying it to the expires_in field of the TokenResponse.
    • Modified the _generate_jwt_token function to use the configurable token lifetime for setting the JWT's expiration (exp) claim.
  • spp_api_v2/tests/test_oauth.py
    • Updated the test_token_generation_success test to reflect the new default token expiration of 24 hours (86400 seconds) instead of 1 hour (3600 seconds).
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for HTTP Basic Authentication and form-encoded bodies to the OAuth token endpoint, which is a great enhancement for RFC compliance and QGIS integration. The implementation looks mostly good, but I have a few suggestions to improve robustness, maintainability, and test coverage. Specifically, I've pointed out some areas where error handling could be improved by adding logging instead of silently passing exceptions, a piece of duplicated code that could be refactored, and a local import that should be moved. Most importantly, the new authentication methods are not covered by automated tests, which is a critical gap to fill before merging.

Comment on lines +78 to +79
except Exception:
pass

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Catching a broad Exception and silently passing is a risky pattern as it can hide unexpected errors and make debugging difficult. While the intent is to fall back to other authentication methods, it's better to at least log the exception to have visibility into why JSON parsing might be failing. This could be due to malformed JSON, a client sending an incorrect data structure, or another unexpected issue.

Suggested change
except Exception:
pass
except Exception as e:
_logger.debug("Could not parse token request from JSON body, falling back: %s", e)
pass

self.assertEqual(data["token_type"], "Bearer")
self.assertIn("expires_in", data)
self.assertEqual(data["expires_in"], 3600) # 1 hour
self.assertEqual(data["expires_in"], 86400) # 24 hours (default)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While this test is correctly updated for the new default token lifetime, the test suite is missing coverage for the new authentication methods introduced in this PR (HTTP Basic Auth and application/x-www-form-urlencoded body). Adding tests for these new code paths is crucial to ensure they work as expected and to prevent future regressions.

basic_client_secret = ""
auth_header = http_request.headers.get("authorization", "")
if auth_header.startswith("Basic "):
import base64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to PEP 8, imports should usually be at the top of the file. Moving this import statement to the top of the file with other imports will improve readability and consistency.

Comment on lines +62 to +63
except (ValueError, UnicodeDecodeError):
pass

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Silently ignoring ValueError or UnicodeDecodeError when decoding the Basic Auth header can make it difficult to debug issues with client requests. It would be beneficial to log these exceptions at a debug level to aid in troubleshooting malformed Authorization headers.

Suggested change
except (ValueError, UnicodeDecodeError):
pass
except (ValueError, UnicodeDecodeError) as e:
_logger.debug("Failed to decode Basic Auth header: %s", e)
pass

Comment on lines +218 to +220
# Read configurable token lifetime (default: 24 hours)
# nosemgrep: odoo-sudo-without-context
token_lifetime_hours = int(env["ir.config_parameter"].sudo().get_param("spp_api_v2.token_lifetime_hours", "24"))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic to retrieve token_lifetime_hours from the configuration is duplicated here and in the get_token function (lines 142-144). This duplication leads to an extra database query and makes the code harder to maintain. Consider refactoring to fetch this value only once in get_token and then pass it as an argument to _generate_jwt_token.

@codecov
Copy link

codecov bot commented Mar 5, 2026

Codecov Report

❌ Patch coverage is 77.41935% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.90%. Comparing base (69e0e5a) to head (7a04d04).

Files with missing lines Patch % Lines
spp_api_v2/routers/oauth.py 77.41% 7 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0      #69      +/-   ##
==========================================
+ Coverage   55.79%   61.90%   +6.11%     
==========================================
  Files         162      373     +211     
  Lines        9291    20561   +11270     
==========================================
+ Hits         5184    12729    +7545     
- Misses       4107     7832    +3725     
Flag Coverage Δ
spp_api_v2 79.96% <77.41%> (?)
spp_api_v2_change_request 60.22% <ø> (?)
spp_api_v2_cycles 71.12% <ø> (?)
spp_api_v2_data 64.41% <ø> (?)
spp_api_v2_entitlements 70.19% <ø> (?)
spp_api_v2_products 66.27% <ø> (?)
spp_api_v2_service_points 70.94% <ø> (?)
spp_api_v2_vocabulary 57.26% <ø> (?)
spp_base_common 90.26% <ø> (ø)
spp_dci_client_dr 55.87% <ø> (?)
spp_dci_client_ibr 60.17% <ø> (?)
spp_dci_demo 69.23% <ø> (?)
spp_dci_server 35.68% <ø> (?)
spp_programs 45.43% <ø> (ø)
spp_security 66.66% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_api_v2/routers/oauth.py 91.13% <77.41%> (ø)

... and 210 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

body = await http_request.json()
return TokenRequest(**body)
except Exception as e:
_logger.debug("Could not parse token request from JSON body, falling back: %s", e)

Check warning

Code scanning / Semgrep OSS

Semgrep Finding: python.lang.security.audit.logging.logger-credential-leak.python-logger-credential-disclosure Warning

Detected a python logger call with a potential hardcoded secret "Could not parse token request from JSON body, falling back: %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging sensitive information.
jeremi added 2 commits March 5, 2026 16:25
Add RFC 6749-compliant client authentication to /oauth/token endpoint:
- HTTP Basic Auth header parsing (Authorization: Basic)
- Form-encoded body support (application/x-www-form-urlencoded)
- Configurable token lifetime via spp_api_v2.token_lifetime_hours (default 24h)
- Keeps JSON body for backwards compatibility

Needed for QGIS native OAuth2 flow, which sends Basic Auth headers.
- Move import base64 to top-level imports
- Add debug logging to Basic Auth decode errors
- Add debug logging to broad except in JSON body parsing
- Remove duplicate token_lifetime_hours fetch from _generate_jwt_token by passing it as a parameter
- Add tests for HTTP Basic Auth and form-encoded body authentication
@jeremi jeremi force-pushed the feat/api-v2-oauth-basic-auth branch from a37283c to 355db0e Compare March 5, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant